package com.abcd.kaixin.api.impl;

import java.io.IOException;
import java.net.URI;
import java.util.List;
import java.util.Map;

import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.type.CollectionType;
import org.codehaus.jackson.map.type.TypeFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.social.UncategorizedApiException;
import org.springframework.social.facebook.api.ImageType;
import org.springframework.social.oauth2.AbstractOAuth2ApiBinding;
import org.springframework.social.oauth2.OAuth2Version;
import org.springframework.social.support.ClientHttpRequestFactorySelector;
import org.springframework.social.support.URIBuilder;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import com.abcd.kaixin.KaixinConstants;
import com.abcd.kaixin.api.FriendOperations;
import com.abcd.kaixin.api.Kaixin;

public class KaixinTemplate extends AbstractOAuth2ApiBinding implements Kaixin {

	
	
	private FriendOperations friendOperations;
	private ObjectMapper objectMapper;
	private String accessToken;
	// private helpers
	private void initialize() {
		// Wrap the request factory with a BufferingClientHttpRequestFactory so
		// that the error handler can do repeat reads on the response.getBody()
		super.setRequestFactory(ClientHttpRequestFactorySelector
				.bufferRequests(getRestTemplate().getRequestFactory()));
		initSubApis();
	}

	private void initSubApis() {
		friendOperations = new FriendTemplate(this, isAuthorized(), getRestTemplate());
	}

	/**
	 * Create a new instance of FacebookTemplate. This constructor creates the
	 * FacebookTemplate using a given access token.
	 * 
	 * @param accessToken
	 *            An access token given by Kaixin after a successful OAuth 2
	 *            authentication (or through Kaixin's JS library).
	 */
	public KaixinTemplate(String accessToken) {
		super(accessToken);
		this.accessToken = accessToken;
		initialize();
	}

	@Override
	public FriendOperations friendOperations() {
		return friendOperations;
	}
	
	// low-level Graph API operations
	public <T> T fetchObject(String objectId, Class<T> type) {
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId).queryParam("access_token", this.accessToken).build();
		MappingJacksonHttpMessageConverter convert = new MappingJacksonHttpMessageConverter() {
			public boolean canRead(Class<?> clazz, MediaType mediaType) {
				return true;
			}
		};
		getRestTemplate().getMessageConverters().add(convert);
		return getRestTemplate().getForObject(uri, type);
	}
	
	public <T> T fetchObject(String objectId, Class<T> type, MultiValueMap<String, String> queryParameters) {
		queryParameters.add("access_token", this.accessToken);
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId).queryParams(queryParameters).build();
		MappingJacksonHttpMessageConverter convert = new MappingJacksonHttpMessageConverter() {
			public boolean canRead(Class<?> clazz, MediaType mediaType) {
				return true;
			}
		};
		getRestTemplate().getMessageConverters().add(convert);
		return getRestTemplate().getForObject(uri, type);
	}

	public <T> List<T> fetchConnections(String objectId, String connectionType, Class<T> type, String... fields) {
		MultiValueMap<String, String> queryParameters = new LinkedMultiValueMap<String, String>();
		if(fields.length > 0) {
			String joinedFields = join(fields);
			queryParameters.set("fields", joinedFields);
		}		
		queryParameters.add("access_token", this.accessToken);
		return fetchConnections(objectId, connectionType, type, queryParameters);
	}

	public <T> List<T> fetchConnections(String objectId, String connectionType, Class<T> type, MultiValueMap<String, String> queryParameters) {
		if (!queryParameters.containsKey("access_token")){
			queryParameters.add("access_token", this.accessToken);
		}
		String connectionPath = connectionType != null && connectionType.length() > 0 ? "/" + connectionType : "";
		URIBuilder uriBuilder = URIBuilder.fromUri(KaixinConstants.baseURL + objectId + connectionPath).queryParams(queryParameters);
		MappingJacksonHttpMessageConverter convert = new MappingJacksonHttpMessageConverter() {
			public boolean canRead(Class<?> clazz, MediaType mediaType) {
				return true;
			}
		};
		getRestTemplate().getMessageConverters().add(convert);
		JsonNode dataNode = getRestTemplate().getForObject(uriBuilder.build(), JsonNode.class);
		return deserializeDataList(dataNode.get("users"), type);
	}

	public byte[] fetchImage(String objectId, String connectionType, ImageType type) {
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId + "/" + connectionType + "?type=" + type.toString().toLowerCase()).build();
		ResponseEntity<byte[]> response = getRestTemplate().getForEntity(uri, byte[].class);
		if(response.getStatusCode() == HttpStatus.FOUND) {
			throw new UnsupportedOperationException("Attempt to fetch image resulted in a redirect which could not be followed. Add Apache HttpComponents HttpClient to the classpath " +
					"to be able to follow redirects.");
		}
		return response.getBody();
	}
	
	@SuppressWarnings("unchecked")
	public String publish(String objectId, String connectionType, MultiValueMap<String, Object> data) {
		MultiValueMap<String, Object> requestData = new LinkedMultiValueMap<String, Object>(data);
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId + "/" + connectionType).build();
		Map<String, Object> response = getRestTemplate().postForObject(uri, requestData, Map.class);
		return (String) response.get("id");
	}
	
	public void post(String objectId, String connectionType, MultiValueMap<String, String> data) {
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId + "/" + connectionType).build();
		getRestTemplate().postForObject(uri, new LinkedMultiValueMap<String, String>(data), String.class);
	}
	
	public void delete(String objectId) {
		LinkedMultiValueMap<String, String> deleteRequest = new LinkedMultiValueMap<String, String>();
		deleteRequest.set("method", "delete");
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId).build();
		getRestTemplate().postForObject(uri, deleteRequest, String.class);
	}
	
	public void delete(String objectId, String connectionType) {
		LinkedMultiValueMap<String, String> deleteRequest = new LinkedMultiValueMap<String, String>();
		deleteRequest.set("method", "delete");
		URI uri = URIBuilder.fromUri(KaixinConstants.baseURL + objectId + "/" + connectionType).build();
		getRestTemplate().postForObject(uri, deleteRequest, String.class);
	}
	
	// AbstractOAuth2ApiBinding hooks
	@Override
	protected OAuth2Version getOAuth2Version() {
		return OAuth2Version.DRAFT_10;
	}

	@Override
	protected MappingJacksonHttpMessageConverter getJsonMessageConverter() {
		MappingJacksonHttpMessageConverter converter = super.getJsonMessageConverter();
		objectMapper = new ObjectMapper();				
		converter.setObjectMapper(objectMapper);		
		return converter;
	}
	
	
		
	
	
	@SuppressWarnings("unchecked")
	private <T> List<T> deserializeDataList(JsonNode jsonNode, final Class<T> elementType) {
		try {
			CollectionType listType = TypeFactory.defaultInstance().constructCollectionType(List.class, elementType);
			return (List<T>) objectMapper.readValue(jsonNode, listType);
		} catch (IOException e) {
			throw new UncategorizedApiException("Error deserializing data from Facebook: " + e.getMessage(), e);
		}
	}
	
	private String join(String[] strings) {
		StringBuilder builder = new StringBuilder();
		if(strings.length > 0) {
			builder.append(strings[0]);
			for (int i = 1; i < strings.length; i++) {
				builder.append("," + strings[i]);
			}
		}
		return builder.toString();
	}
	
	
}
